using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceProcess;
using System.Configuration;
using System.Diagnostics;
using System.Threading;
using System.IO;
using System.Reflection;

namespace DirectoryWatcher
{
    /// <summary>
    /// The main logic class for the DirectoryWatcher service:  it reads a collection of 
    /// directories and file sets from the configuration file and then monitors them for specific 
    /// types of filesystem changes.  When these changes are detected, one or more specified 
    /// programs are invoked.
    /// </summary>
    public class Program : ServiceBase
    {
        /// <summary>
        /// Configuration section that contains the directories/files to watch and the programs to
        /// run.
        /// </summary>
        private WatchInformation watchInformation = null;
        /// <summary>
        /// Collection of FileSystemWatcher objects that we create.
        /// </summary>
        private Dictionary<string, Dictionary<string, FileSystemWatcher>> watchers = new Dictionary<string, Dictionary<string, FileSystemWatcher>>();
        /// <summary>
        /// Maximum number of concurrent processes that can be active.
        /// </summary>
        private uint maxConcurrentProcesses = 10;
        /// <summary>
        /// Resource-centric semaphore object to control the number of active processes.
        /// </summary>
        private static CountingSemaphore executionRegulator = null;

        /// <summary>
        /// Entry point for the service.
        /// </summary>
        public static void Main()
        {
            // Set a generic exception handler so that we can log all unhandled exceptions that are
            // thrown
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

            ServiceBase[] servicesToRun;

            servicesToRun = new ServiceBase[]{new Program()};
            ServiceBase.Run(servicesToRun);
        }

        /// <summary>
        /// Generic exception handler that logs all unhandled exceptions that are thrown during 
        /// execution.
        /// </summary>
        /// <param name="sender">
        /// Object from which this event originated.
        /// </param>
        /// <param name="e">
        /// Exception that was thrown.
        /// </param>
        protected static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            Exception exception = (Exception)e.ExceptionObject;
            WriteToEventLog(EventLogEntryType.Error, "An unhandled exception was generated during execution.\n\nType: {1}\nMessage:\n{2}", exception.GetType().FullName, exception.Message);
        }

        /// <summary>
        /// Handler for the service's Start event; reads the configuration file and instantiates a
        /// FileSystemWatcher object for each directory/file set that we're supposed to watch.
        /// </summary>
        /// <param name="args">
        /// Any arguments passed into the service (currently unused).
        /// </param>
        protected override void OnStart(string[] args)
        {
            WriteToEventLog(EventLogEntryType.Information, "Starting up the Directory Watcher service.");

            try
            {
                // Get the section from the configuration file that contains the directories/file 
                // sets that we are to watch
                watchInformation = (WatchInformation)ConfigurationManager.GetSection("watchInformation");

                // Get the maximum number of concurrent processes that can be active (if specified)
                if (ConfigurationManager.AppSettings["maxConcurrentProcesses"] != null)
                    maxConcurrentProcesses = Convert.ToUInt32(ConfigurationManager.AppSettings["maxConcurrentProcesses"]);

                WriteToEventLog(EventLogEntryType.Information, "Using a concurrent process count of {0}.", maxConcurrentProcesses);

                // Instantiate the regulation semaphore to enforce the maximum process count
                executionRegulator = new CountingSemaphore(maxConcurrentProcesses);

                foreach (DirectoryToWatch directoryToWatch in watchInformation.DirectoriesToWatch)
                {
                    foreach (FileSetToWatch fileSetToWatch in directoryToWatch.FileSetsToWatch)
                    {
                        // If we're using any runtime-compiled code, validate each assembly that 
                        // was generated to make sure that only one class exists in it that 
                        // implements the IFileSystemEventHandler interface
                        foreach (ProgramToExecute programToExecute in fileSetToWatch.ProgramsToExecute)
                        {
                            if (programToExecute.Code.Text != null)
                                ValidateAssembly(programToExecute.Code.Assembly);
                        }

                        // Create and instantiate an individual FileSystemWatcher for each wildcard
                        // file set and a single FileSystemWatcher object to handle all regular 
                        // expression file sets
                        if (fileSetToWatch.MatchExpressionType == MatchExpressionType.Wildcard || watchers[directoryToWatch.Path][""] == null)
                        {
                            FileSystemWatcher watcher = new FileSystemWatcher(directoryToWatch.Path);

                            // Set the filter to the match expression for wildcard file sets, and
                            // blank (to capture changes for all files and do the actual matching 
                            // in the event handler) for regular expression file sets
                            watcher.Filter = (fileSetToWatch.MatchExpressionType == MatchExpressionType.Wildcard ? fileSetToWatch.MatchExpression : "");

                            // Attach handlers to the various filesystem events that we're supposed
                            // to watch for
                            if (fileSetToWatch.EventsToWatch["All"] != null)
                            {
                                watcher.Changed += new FileSystemEventHandler(watcher_OnChanged);
                                watcher.Created += new FileSystemEventHandler(watcher_OnChanged);
                                watcher.Deleted += new FileSystemEventHandler(watcher_OnChanged);
                                watcher.Renamed += new RenamedEventHandler(watcher_OnChanged);
                            }

                            else
                            {
                                foreach (EventToWatch eventToWatch in fileSetToWatch.EventsToWatch)
                                {
                                    if (eventToWatch.Type == WatcherChangeTypes.Changed)
                                        watcher.Changed += new FileSystemEventHandler(watcher_OnChanged);

                                    else if (eventToWatch.Type == WatcherChangeTypes.Created)
                                        watcher.Created += new FileSystemEventHandler(watcher_OnChanged);

                                    else if (eventToWatch.Type == WatcherChangeTypes.Deleted)
                                        watcher.Deleted += new FileSystemEventHandler(watcher_OnChanged);

                                    else if (eventToWatch.Type == WatcherChangeTypes.Renamed)
                                        watcher.Renamed += new RenamedEventHandler(watcher_OnChanged);
                                }
                            }

                            // Create a new dictionary entry for this directory path if it doesn't
                            // exist already
                            if (!watchers.ContainsKey(directoryToWatch.Path))
                                watchers[directoryToWatch.Path] = new Dictionary<string, FileSystemWatcher>();

                            // Add the watcher to the list for this directory and enable it
                            watchers[directoryToWatch.Path][watcher.Filter] = watcher;
                            watcher.EnableRaisingEvents = true;
                        }

                        WriteToEventLog(EventLogEntryType.Information, "Added watcher for the path \"{0}\" and the {1} \"{2}\".", directoryToWatch.Path, (fileSetToWatch.MatchExpressionType == MatchExpressionType.Wildcard ? "wildcard expression" : "regular expression"), fileSetToWatch.MatchExpression);
                    }
                }
            }

            // Log any exceptions that occur during startup
            catch (Exception exception)
            {
                WriteToEventLog(EventLogEntryType.Error, "Exception occurred while starting the service.\n\nType: {0}\nMessage:\n{1}", exception.GetType().FullName, exception.Message);
                throw;
            }

            base.OnStart(args);
        }

        /// <summary>
        /// Handler for the service's Stop event; stops any active FileSystemWatcher objects.
        /// </summary>
        protected override void OnStop()
        {
            WriteToEventLog(EventLogEntryType.Information, "Stopping the Directory Watcher service.");

            // Stop each FileSystemWatcher object
            foreach (string directory in watchers.Keys)
            {
                foreach (string filter in watchers[directory].Keys)
                    watchers[directory][filter].EnableRaisingEvents = false;
            }

            base.OnStop();
        }

        /// <summary>
        /// Event handler for the Created, Changed, and Deleted filesystem events.
        /// </summary>
        /// <param name="source">
        /// FileSystemWatcher object for which the event was raised.
        /// </param>
        /// <param name="e">
        /// Arguments associated with the filesystem change.
        /// </param>
        protected void watcher_OnChanged(object source, FileSystemEventArgs e)
        {
            FileSystemWatcher watcher = (FileSystemWatcher)source;

            // If this watcher is a wildcard watcher, grab its programs from the configuration
            // section
            if (watcher.Filter != "")
                AddProgramsToQueue(watchInformation.DirectoriesToWatch[watcher.Path].FileSetsToWatch[watcher.Filter].ProgramsToExecute, e.FullPath, e.ChangeType);

            // Otherwise, go through the list of regular expression file sets for this directory, 
            // see if any of them match the file that was modified, and, if they do, get their 
            // programs from the configuration section
            else
            {
                foreach (FileSetToWatch fileSetToWatch in watchInformation.DirectoriesToWatch[watcher.Path].FileSetsToWatch)
                {
                    if (fileSetToWatch.MatchExpressionType == MatchExpressionType.RegularExpression && fileSetToWatch.MatchRegex.IsMatch(e.Name))
                        AddProgramsToQueue(fileSetToWatch.ProgramsToExecute, e.FullPath, e.ChangeType);
                }
            }
        }

        /// <summary>
        /// Creates ProcessStartInfo or IFileSystemEventHandler objects for each of the programs 
        /// responsible for responding to an event and then spins off a thread to execute each of 
        /// those programs in sequence.
        /// </summary>
        /// <param name="programsToExecute">
        /// List of programs that need to be executed.
        /// </param>
        /// <param name="filePath">
        /// Path to the file for which the event was raised.
        /// </param>
        /// <param name="eventType">
        /// The filesystem event type for which the program is being executed.
        /// </param>
        protected void AddProgramsToQueue(ProgramToExecuteCollection programsToExecute, string filePath, WatcherChangeTypes eventType)
        {
            List<ExecutionInstance> executionInstances = new List<ExecutionInstance>();
            Thread executionThread = new Thread(new ParameterizedThreadStart(RunPrograms));

            // Loop through each program and create an ExecutionInstance object for it
            foreach (ProgramToExecute programToExecute in programsToExecute)
            {
                ExecutionInstance executionInstance;

                // If we're running a pre-compiled application, create the necessary 
                // ProcessStartInfo object
                if (programToExecute.Code.Text == null)
                {
                    ProcessStartInfo startInfo = new ProcessStartInfo(programToExecute.Path);
                    FileInfo fileInfo = new FileInfo(filePath);

                    // Flesh out the arguments through string substitution
                    startInfo.Arguments = programToExecute.Arguments.Replace("{P}", filePath);
                    startInfo.Arguments = startInfo.Arguments.Replace("{F}", fileInfo.Name);
                    startInfo.Arguments = startInfo.Arguments.Replace("{E}", fileInfo.Extension);
                    startInfo.Arguments = startInfo.Arguments.Replace("{D}", fileInfo.DirectoryName);

                    if (fileInfo.Extension != "")
                        startInfo.Arguments = startInfo.Arguments.Replace("{f}", fileInfo.Name.Substring(0, fileInfo.Name.Length - fileInfo.Extension.Length - 1));

                    else
                        startInfo.Arguments = startInfo.Arguments.Replace("{f}", fileInfo.Name);

                    startInfo.UseShellExecute = false;
                    startInfo.RedirectStandardInput = programToExecute.RedirectFileToStdin;

                    executionInstance = new ExecutionInstance(startInfo, eventType, filePath, programToExecute.RedirectFileToStdin);
                }

                // Otherwise, we're using runtime-compiled code and we need to create an instance 
                // of the class that implements IFileSystemEventHandler
                else
                {
                    IFileSystemEventHandler eventHandler = CreateEventHandlerInstance(programToExecute.Code.Assembly);
                    executionInstance = new ExecutionInstance(eventHandler, eventType, filePath);
                }

                executionInstances.Add(executionInstance);
            }

            // Start the thread that will execute the programs
            executionThread.Start(executionInstances);
        }

        /// <summary>
        /// Runs a specified list of processes, one after the other.
        /// </summary>
        /// <param name="source">
        /// List containing ExecutionInstance objects for each process.
        /// </param>
        public static void RunPrograms(object source)
        {
            List<ExecutionInstance> executionInstances = (List<ExecutionInstance>)source;

            // If we're watching for a create event, we first try to open the file in exclusive 
            // mode; this is to account for the "long copy" scenario where the create event is 
            // fired when the copy first starts, but we need to wait until the copy completes 
            // before we begin our processing
            if (executionInstances[0].EventType == WatcherChangeTypes.Created)
            {
                FileStream fileStream = null;

                while (fileStream == null)
                {
                    try
                    {
                        fileStream = File.Open(executionInstances[0].FilePath, FileMode.Open, FileAccess.Read, FileShare.None);
                    }

                    // Catch the IOException that will be thrown when we fail to open the file in 
                    // exclusive mode
                    catch (IOException exception)
                    {
                        string warningTrap = exception.Message;
                        Thread.Sleep(1000);
                    }

                    // Log any other unhandled exceptions that are thrown
                    catch (Exception exception)
                    {
                        WriteToEventLog(EventLogEntryType.Error, "Unhandled exception occurred while waiting for \"{0}\" to become available.\n\nType: {1}\nMessage:\n{2}", executionInstances[0].FilePath, exception.GetType().FullName, exception.Message);
                    }
                }

                fileStream.Close();
            }

            // Claim a resource from the counting semaphore and enter the critical section
            executionRegulator.P();

            foreach (ExecutionInstance executionInstance in executionInstances)
            {
                try
                {
                    // If we're running a pre-compiled application, start the process
                    if (executionInstance.EventHandler == null)
                    {
                        WriteToEventLog(EventLogEntryType.Information, "Running program in response to event for {3} being {4}:\n\"{0}\"{1}{2}.", executionInstance.StartInfo.FileName, (executionInstance.StartInfo.Arguments != "" ? " " + executionInstance.StartInfo.Arguments : ""), (executionInstance.RedirectFileToStdin ? " < \"" + executionInstance.FilePath + "\"" : ""), executionInstance.FilePath, executionInstance.EventType.ToString().ToLower());

                        Process executionProcess = Process.Start(executionInstance.StartInfo);

                        // If we're redirecting the file to the standard input stream, open it up
                        // and read its contents into the stream in 1 KB chunks
                        if (executionInstance.RedirectFileToStdin)
                        {
                            BinaryReader binaryReader = new BinaryReader(File.Open(executionInstance.FilePath, FileMode.Open));
                            BinaryWriter binaryWriter = new BinaryWriter(executionProcess.StandardInput.BaseStream);
                            byte[] buffer = new byte[1024];
                            int readSize = binaryReader.Read(buffer, 0, buffer.Length);

                            while (readSize != 0)
                            {
                                binaryWriter.Write(buffer, 0, readSize);
                                readSize = binaryReader.Read(buffer, 0, buffer.Length);
                            }

                            binaryReader.Close();
                            binaryWriter.Close();
                        }

                        // Wait for the process to exit and then clean it up
                        executionProcess.WaitForExit();
                        executionProcess.Close();
                    }

                    // Otherwise, invoke the OnFileSystemEvent() method for the handler class 
                    // defined in the runtime-compiled code
                    else
                    {
                        WriteToEventLog(EventLogEntryType.Information, "Invoking {0}.OnFileSystemEvent() in response to event for {1} being {2}.", executionInstance.EventHandler.GetType().Name, executionInstance.FilePath, executionInstance.EventType.ToString().ToLower());

                        FileInfo fileInfo = new FileInfo(executionInstance.FilePath);
                        FileSystemEventArgs eventArguments = new FileSystemEventArgs(executionInstance.EventType, fileInfo.DirectoryName, fileInfo.Name);

                        executionInstance.EventHandler.OnFileSystemEvent(eventArguments);
                    }
                }

                // Log any exceptions that occur while running the program
                catch (Exception exception)
                {
                    WriteToEventLog(EventLogEntryType.Error, "Exception occurred while running {0}.\n\nType: {1}\nMessage:\n{2}", (executionInstance.StartInfo != null ? "\"" + executionInstance.StartInfo.FileName + "\"" : executionInstance.EventHandler.GetType().Name + ".OnFileSystemEvent()"), exception.GetType().FullName, exception.Message);
                }
            }

            // Release the resource and exit the critical section
            executionRegulator.V();
        }

        /// <summary>
        /// Writes a formatted string message to the event log.
        /// </summary>
        /// <param name="messageType">
        /// Event log entry type for this message.
        /// </param>
        /// <param name="message">
        /// String.Format() compatible string representing the contents of the message.
        /// </param>
        /// <param name="arguments">
        /// Arguments, if any, to pass into String.Format() along with the message parameter.
        /// </param>
        protected static void WriteToEventLog(EventLogEntryType messageType, string message, params object[] arguments)
        {
            message = String.Format(message, arguments);

            EventLog eventLog = new EventLog("Application", Environment.MachineName, "Directory Watcher");
            eventLog.WriteEntry(message, messageType);
        }

        /// <summary>
        /// Validates an assembly generated from runtime-compiled code; makes sure that there is
        /// one and only one class that implements the IFileSystemEventHandler interface.
        /// </summary>
        /// <param name="assembly">
        /// Assembly object that we are to check.
        /// </param>
        protected static void ValidateAssembly(Assembly assembly)
        {
            FindEventHandlerType(assembly);
        }

        /// <summary>
        /// Creates an instance of the event handler class (class that implements the 
        /// IFileSystemEventHandler interface) that's defined in a snippet of runtime-compiled 
        /// code.
        /// </summary>
        /// <param name="assembly">
        /// Assembly containing a handler class.
        /// </param>
        /// <returns>
        /// An instance of the event handler class.
        /// </returns>
        protected static IFileSystemEventHandler CreateEventHandlerInstance(Assembly assembly)
        {
            Type eventHandlerType = FindEventHandlerType(assembly);
            return (IFileSystemEventHandler)Activator.CreateInstance(eventHandlerType);
        }

        /// <summary>
        /// Searches an assembly for a type that implements the IFileSystemEventHandler interface 
        /// and throws exceptions if more or less than one are found.
        /// </summary>
        /// <param name="assembly">
        /// Assembly object that we are to search.
        /// </param>
        /// <returns>
        /// The Type object for the class that implements IFileSystemEventHandler.
        /// </returns>
        protected static Type FindEventHandlerType(Assembly assembly)
        {
            Type eventHandlerType = null;

            foreach (Type type in assembly.GetTypes())
            {
                if (type.GetInterface("IFileSystemEventHandler") != null)
                {
                    // If we've already found a qualifying type, then throw an exception
                    if (eventHandlerType != null)
                        throw new ArgumentException(String.Format("Multiple classes implementing IFileSystemEventHandler were found in {0}.", assembly.FullName));

                    eventHandlerType = type;
                }
            }

            // If no qualifying types were found, then throw an exception
            if (eventHandlerType == null)
                throw new ArgumentException(String.Format("No classes implementing IFileSystemEventHandler were found in {0}.", assembly.FullName));

            return eventHandlerType;
        }

        /// <summary>
        /// Sets member variables and properties for the class.
        /// </summary>
        private void InitializeComponent()
        {
            this.ServiceName = "DirectoryWatcher";
        }
    }
}
